/** for Serializable */
import java.io.Serializable;

/** for ArrayList */
import java.util.ArrayList;
import java.util.List;

/** for Arrays */
import java.util.Arrays;

/** for hashMap */
import java.util.HashMap;

/** for Random */
import java.util.Random;


/**
 * This is a class to handle ship.
 * @author Code Traders
 * @version 1.0
 */
public class Ship implements Serializable {
	
	/**
	 * serialVersionUID for the Ship class
	 */
	private static final long serialVersionUID = -8871784851552905231L;
	
	/**
	 * array holding the goods
	 */
	private int[] goods = new int[Goods.values().length];
	
	/**
	 * current money
	 */
	private int money;
	
	/**
	 * current Ship type
	 */
	private Ships shipType;
	
	/**
	 * current cargo space
	 */
	private int cargoSpace;
	
	/**
	 * current planet
	 */
	private Planet currentPlanet, tempPlanet;
	
	/**
	 * current fuel value
	 */
	private double fuel;
	
	/**
	 * current life value
	 */
	private double life;
	
	/**
	 * Current list of weapons in the ship
	 */
	private List<Weapon> weapons;
	
	/**
	 * Current list of shields in the ship
	 */
	private HashMap<Shield, Integer> shields;
	
	/**
	 * Current list of gadgets in the ship
	 */
	private List<Gadget> gadgets;
	
	/** for TWO **/
	private static final int TWO = 2;
	
	/** for THREE **/
	private static final int THREE = 3;
	
	/** for FOUR **/
	private static final int FOUR = 4;
	
	/** for FIVE **/
	private static final int FIVE = 5;

	/** for SIX **/
	private static final int SIX = 6;
	
	/** for SEVEN **/
	private static final int SEVEN = 7;
	
	/** for EIGHT **/
	private static final int EIGHT = 8;
	
	/** for NINE **/
	private static final int NINE = 9;
	
	/** for TEN **/
	private static final int TEN = 10;

	/** for FIFTEEN **/
	private static final int FIFTEEN = 15;
	
	/** for THOUSAND **/
	private static final int THOUSAND = 1000;
	
	/**
	 * Constructor for Ship.
	 * Sets the default money the Ship has to 5000, 
	 * the type of the Ship to GNAT, 
	 * the initial cargo space filled to 0. 
	 */
	public Ship() {
		this.money = THOUSAND;
		this.shipType = Ships.GNAT;
		this.cargoSpace = 0;
		this.fuel = shipType.getMaxFuel();
		this.life = shipType.getHullStrength();
		this.weapons = new ArrayList<Weapon>(shipType.getWeaponSlots());
		this.shields = new HashMap<Shield, Integer>(shipType.getShieldSlots());
		this.gadgets = new ArrayList<Gadget>(shipType.getGadgetSlots());
	}
	
	/**
	 * to get the life
	 * @return life
	 */
	public double getLife() {
		return this.life;
	}
	
	/**
	 * to set the life
	 * @param n
	 */
	public void setLife(double n) {
		this.life = n > this.shipType.getHullStrength() ? 
					this.shipType.getHullStrength() : n;
	}
	
	/**
	 * to change ship
	 * @param s
	 */
	public void changeShip(Ships s) {
		this.shipType = s;
		this.weapons = new ArrayList<Weapon>(shipType.getWeaponSlots());
		this.shields = new HashMap<Shield, Integer>(shipType.getShieldSlots());
		this.gadgets = new ArrayList<Gadget>(shipType.getGadgetSlots());
		this.cargoSpace = 0;
		this.fuel = shipType.getMaxFuel();
		this.life = shipType.getHullStrength();
	}
	
	/**
	 * to check whether user can buy ship or not
	 * @param s
	 * @return if user can buy return true, otherwise false
	 */
	public boolean canBuyShip(Ships s) {
		return (money + shipType.getPrice()) >= s.getPrice();
	}
	
	/**
	 * to get the random item
	 * @return random item
	 */
	public Item getRandomItem() {
		final Random rand = new Random();
		final List<Shield> shieldsList = new ArrayList<Shield>(shields.size());
		shieldsList.addAll(shields.keySet());
		if (weapons.size() != 0) {
			return weapons.get(rand.nextInt(weapons.size()));
		} else if (shields.size() != 0) {
			return shieldsList.get(rand.nextInt(shields.size()));
		} else if (gadgets.size() != 0) {
			return gadgets.get(rand.nextInt(gadgets.size()));
		} else {
			return null;
		}
	}
	
	/**
	 * to remove item
	 * @param i
	 */
	public void removeItem(Item i) {
		if (i instanceof Weapon) {
			weapons.remove(i);
		}
	}
	
	/**
	 * to add weapon
	 * @param w
	 * @return if weapon is added true, otherwise false
	 */
	public boolean canAddWeapon(Weapon w) {
		if (weapons.size() < shipType.getWeaponSlots()) {
			weapons.add(w);
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * to remove weapon
	 * @param w
	 */
	public void removeWeapon(Weapon w) {
		weapons.remove(w);
	}
	
	/**
	 * to check whether weapon can be added or not
	 * @return true or false
	 */
	public boolean canAddWeapon() {
		return weapons.size() < shipType.getWeaponSlots();
	}
	
	/**
	 * to check user has certain weapon or not
	 * @param w
	 * @return if user has it return true, otherwise false
	 */
	public boolean hasWeapon(Weapon w) {
		return weapons.contains(w);
	}
	
	/**
	 * to add shield
	 * @param s
	 * @return if shield is added true, otherwise false;
	 */
	public boolean canAddShield(Shield s) {
		if (shields.size() < shipType.getShieldSlots()) {
			shields.put(s, s.getStrength());
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * to remove shield
	 * @param s
	 */
	public void removeShield(Shield s) {
		shields.remove(s);
	}
	
	/**
	 * to check whether shield can be added
	 * @return if can be added return true, otherwise return false
	 */
	public boolean canAddShield() {
		return shields.size() < shipType.getShieldSlots();
	}
	
	/**
	 * to check ship has certain shield or not
	 * @param s
	 * @return if it has it return true, otherwise return false
	 */
	public boolean hasShield(Shield s) {
		return shields.containsKey(s);
	}
	
	/**
	 * to add Gadget
	 * @param g
	 * @return if gadget is added return true, otherwise return false
	 */
	public boolean canAddGadget(Gadget g) {
		if (gadgets.size() < shipType.getGadgetSlots()) {
			gadgets.add(g);
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * to remove gadget
	 * @param g
	 */
	public void removeGadget(Gadget g) {
		gadgets.remove(g);
	}
	
	/**
	 * to check whether ship can have gadget or not
	 * @return if it can have it return true, otherwise false;
	 */
	public boolean canAddGadget() {
		return gadgets.size() < shipType.getGadgetSlots();
	}
	
	/**
	 * to check ship has certain gadget or not
	 * @param g
	 * @return if it has it return true, otherwise false;
	 */
	public boolean hasGadget(Gadget g) {
		return gadgets.contains(g);
	}
	
	/**
	 * to check this ship has how many weapons
	 * @return number of items
	 */
	public int[] numberOfItems() {
		int[] numberOfItems = new int[TEN];
		numberOfItems = getNumberOfWeapons(numberOfItems);
		numberOfItems = getNumberOfShields(numberOfItems);
		numberOfItems = getNumberOfGadgets(numberOfItems);
		return numberOfItems;
	}
	
	/**
	 * to get the number  of Weapons
	 * @param numberOfItems
	 * @return number of weapons
	 */
	private int[] getNumberOfWeapons(int[] numberOfItems) {
		for (Weapon w : weapons) {
			if (w.equals(Weapon.PULSE)) {
				numberOfItems[0]++;
			} else if (w.equals(Weapon.BEAM)) {
				numberOfItems[1]++;
			} else if (w.equals(Weapon.MILITARY)) {
				numberOfItems[TWO]++;
			}
		}
		return numberOfItems;
	}
	
	/**
	 * to get the number of Shields
	 * @param numberOfItems
	 * @return number of shields
	 */
	private int[] getNumberOfShields(int[] numberOfItems) {
		for (Shield s : shields.keySet()) {
			if (s.equals(Shield.ENERGY)) {
				numberOfItems[THREE]++;
			} else if (s.equals(Shield.REFLECTIVE)) {
				numberOfItems[FOUR]++;
			}
		}
		return numberOfItems;
	}
	
	/**
	 * to get the number of Gadgets
	 * @param numberOfItems
	 * @return number of gadgets
	 */
	private int[] getNumberOfGadgets(int[] numberOfItems) {
		for (Gadget g : gadgets) {
			if (g.equals(Gadget.EXTRACARGO)) {
				numberOfItems[FIVE]++;
			} else if (g.equals(Gadget.NAVIGATION)) {
				numberOfItems[SIX]++;
			} else if (g.equals(Gadget.AUTOREPAIR)) {
				numberOfItems[SEVEN]++;
			} else if (g.equals(Gadget.TARGET)) {
				numberOfItems[EIGHT]++;
			} else if (g.equals(Gadget.CLOAK)) {
				numberOfItems[NINE]++;
			}
		}
		return numberOfItems;
	}
	
	/**
	 * to check the number of specific items ship has
	 * @param item
	 * @return the number of specific items
	 */
	public int hasNumberOfItems(Item item) {
		int num = 0;
		if (item instanceof Weapon) {
			for (Weapon w : weapons) {
				if (w.equals(item)) num++;
			}
		} else if (item instanceof Shield) {
			for (Shield s : shields.keySet()) {
				if (s.equals(item)) num++;
			}
		} else if (item instanceof Gadget) {
			for (Gadget g : gadgets) {
				if (g.equals(item)) num++;
			}
		}
		return num;
	}
	
	/**
	 * to calculate the damage 
	 * @return damage
	 */
	public int getDamage() {
		int num = 0;
		for (Weapon w : weapons) {
			num += w.getDamage();
		}
		num += hasNumberOfItems(Gadget.TARGET) * FIVE;
		return num;
	}
	
	/**
	 * to calculate how much damage ship can defense
	 * @return n
	 */
	public int getDefense() {
		int num = 0;
		for (Shield s : shields.keySet()) {
			num += s.getStrength();
		}
		return num;
	}
	
	/**
	 * to calculate how much damage ship will be damaged
	 * @param n
	 */
	public void calcAttack(double n) {
		while (n != 0 && !shields.isEmpty()) {
			Shield firstShield = (Shield) shields.keySet().toArray()[0];
			int tmp = shields.get(firstShield) - 1;
			shields.put(firstShield, tmp);
			final int zero = 0;
			if (shields.get(firstShield).equals(new Integer(zero))) {
				shields.remove(firstShield);
			}
		}
		life -= n;
	}
	
	/**
	 * Getter for the array of Goods the Ship has
	 * 
	 * @return the array of Goods the Ship has
	 */
	public int[] getGoods() {
		return this.goods;
	}
	
	/**
	 * -- NOT USED --
	 * 
	 * Setter for the Goods
	 * 
	 * @param g array of Goods
	 */
	public void setGoods(int[] g) {
		this.goods = g.clone();
	}
	
	/**
	 * Add the appropriate number of Goods to the cargo
	 * 
	 * @param g Goods to add
	 * @param n number of Goods to add
	 */
	public void addGoods(Goods g, int n) {
		this.cargoSpace += n;
		this.goods[g.ordinal()] += n;
	}
	
	/**
	 * Remove the appropriate number of Goods from the cargo
	 * 
	 * @param g Goods to remove
	 * @param n number of Goods to remove
	 */
	public void removeGoods(Goods g, int n) {
		this.cargoSpace -= n;
		this.goods[g.ordinal()] -= n;
	}
	
	/**
	 * Getter for the money
	 * 
	 * @return money the Ship has
	 */
	public int getMoney() {
		return this.money;
	}
	
	/**
	 * Setter for the money
	 * 
	 * @param m money
	 */
	public void setMoney(int m) {
		this.money = m;
	}
	
	/**
	 * Add appropriate amount of money to the Ship
	 * 
	 * @param m amount of money to add
	 */
	public void addMoney(int m) {
		this.money += m;
	}
	
	/**
	 * Subtract appropriate amount of money from the Ship
	 * 
	 * @param m amount of money to subtract
	 */
	public void subtractMoney(int m) {
		this.money -= m;
	}
	
	/**
	 * Getter for the current type of the ship
	 * 
	 * @return the current type of the ship
	 */
	public Ships getShipType() {
		return this.shipType;
	}
	
	/**
	 * this is to set shiptype but not used.
	 * @param s
	 */
	public void setShipType(Ships s) {
		this.shipType = s;
	}
	
	/**
	 * Returns if the cargo has enough space left for n
	 * 
	 * @param n number of items to be added
	 * @return true if the cargo has enough space left for n, false otherwise
	 */
	public boolean isCargoSpaceAvailable(int n) {
		return cargoSpace + n <= shipType.getMaxCargoSpace()
				+ hasNumberOfItems(Gadget.EXTRACARGO) * FIVE;
	}

	/**
	 * Getter for the cargoSpace
	 * 
	 * @return cargoSpace
	 */
	public int getCargoSpace() {
		return cargoSpace - hasNumberOfItems(Gadget.EXTRACARGO) * FIVE;
	}

	/**
	 * Setter for the cargoSpace
	 * 
	 * @param cargoSpace cargoSpace
	 */
	public void setCargoSpace(int cargoSpace) {
		this.cargoSpace = cargoSpace;
	}
	
	/**
	 * to get current planet
	 * @return current planet
	 */
	public Planet getCurrentPlanet() {
		return this.currentPlanet;
	}
	
	/**
	 * to set current planet
	 * @param p
	 */
	public void setCurrentPlanet(Planet p) {
		this.currentPlanet = p;
	}
	
	/**
	 * to get current planet
	 * @return current planet
	 */
	public Planet getTempPlanet() {
		return this.tempPlanet;
	}
	
	/**
	 * to set current planet
	 * @param p
	 */
	public void setTempPlanet(Planet p) {
		this.tempPlanet = p;
	}
	
	/**
	 * when move to other planet
	 * @param p
	 */
	public void moveToPlanet(Planet p) {
		this.fuel -= getDistance(currentPlanet, p) /
				FIFTEEN + hasNumberOfItems(Gadget.NAVIGATION) * FIVE;
		this.currentPlanet = p;
	}
	
	/**
	 * to add fuel
	 * @param n
	 */
	public void addFuel(int n) {
		this.fuel += n;
	}
	
	/**
	 * to show how fuel is consumed
	 * @param n
	 */
	public void subtractFuel(int n) {
		this.fuel -= n;
	}
	
	/**
	 * to get fuel
	 * @return fuel
	 */
	public double getFuel() {
		return this.fuel;
	}
	
	/**
	 * to set fuel
	 * @param n
	 */
	public void setFuel(double n) {
		this.fuel = n > shipType.getMaxFuel() ? shipType.getMaxFuel() : n;
	}
	
	/**
	 * to check user can travel to other planet
	 * @param p1
	 * @param p2
	 * @return if can true, otherwise false
	 */
	public boolean canTravelTo(Planet p1, Planet p2) {
		final int four = FOUR;
		return getDistance(p1, p2) <= this.fuel * FIFTEEN +
				(this.fuel * FIFTEEN * hasNumberOfItems(Gadget.NAVIGATION) * four);
	}
	
	/**
	 * go get travel distance
	 * @param p1
	 * @param p2
	 * @return distance
	 */
	private double getDistance(Planet p1, Planet p2) {
		return Math.sqrt(Math.pow(p1.getPoint().getX() -
				p2.getPoint().getX(), TWO) + Math.pow
				(p1.getPoint().getY() - p2.getPoint()
						.getY(), TWO));
	}
	
	/**
	 * This is to check equality between ships stuff and market stuffs
	 * @param obj
	 * @return true or false.
	 */
	@Override
	public boolean equals(Object obj) {
		
		if (obj == this) return true;
		final Ship ship = obj instanceof Ship ? (Ship) obj : null;
		return ship != null && this.isTheSameShip(ship);
	}
	
	/**
	 * to check this is the same ship or not
	 * @param ship
	 * @return same return true otherwise false
	 */
	private boolean isTheSameShip(Ship ship) {
		return Arrays.equals(this.goods, ship.goods) 
				&& this.money == ship.money && this.shipType == ship.shipType 
				&& this.cargoSpace == ship.cargoSpace &&
				this.currentPlanet.equals(ship.currentPlanet) 
				&& new Float(this.fuel).equals(new Float(ship.fuel))
				&& new Float(this.life).equals(new Float(ship.life))
				&& this.weapons.equals(ship.weapons) &&
				this.shields.keySet().equals(ship.shields.keySet()) 
				&& Arrays.equals(this.shields.values().toArray(),
								 ship.shields.values().toArray())
				&& this.gadgets.equals(ship.gadgets);
	}
	
	/**
	 * returns hashcode of the object
	 * 
	 * @return hash code of the object
	 */
	@Override
	public int hashCode() {
		return super.hashCode();
	}
	
	/**
	 * This is a tostring method.
	 * @return tostring.
	 */
	@Override
	public String toString() {
		return super.toString();
	}
}
